package org.microsleep;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import android.util.Log;

public abstract class Session {
	private ORMUtil util;
	private Map<PersistentClass, Object> storeStatements = new HashMap<PersistentClass, Object>();
	private Map<PersistentClass, Object> loadStatements = new HashMap<PersistentClass, Object>();
	private Map<Reference, Object> sessionCache = new HashMap<Reference, Object>();

	public Session(ORMUtil util) {
		super();
		this.util = util;
	}

	public void createTable(Class<?> entityClass) {
		PersistentClass pClass = util.getMappingFor(entityClass);
		String createTableStatement = util.getCreateTableStatement(pClass);
		Object stmt = compileCreateTable(createTableStatement);
		Log.d(getClass().getName(), "Compiled new CREATE TABLE-Statement for "
				+ entityClass.getName() + ": " + createTableStatement);
		executeStatement(stmt);
	}

	public void store(Object object) {
		_store(object, new HashSet<Reference>());
	}

	private void _store(Object object, Set<Reference> stored) {
		PersistentClass pClass = util.getMappingFor(object.getClass());
		Reference reference = new Reference(pClass, pClass.getIdentifier()
				.getValue(object));
		if (stored.contains(reference)) {
			return;
		}
		stored.add(reference);
		Log.d(getClass().getName(), "Storing " + reference);
		Object stmt = storeStatements.get(pClass);
		if (stmt == null) {
			String storeStatement = util.getStoreStatement(pClass,
					pClass.getProperties());
			Log.d(getClass().getName(), "Compiled new STORE-Statement for "
					+ object.getClass() + ": " + storeStatement);
			stmt = compileStore(storeStatement);
			storeStatements.put(pClass, stmt);
		}
		Map<Property, Object> values = util.getValues(pClass.getProperties(),
				object);
		for (Property property : values.keySet()) {
			Type type = property.getType();
			if (type instanceof PersistentClass) {
				if (property.getCascades().contains(CascadeOperation.STORE)) {
					Object relObject = values.get(property);
					if (relObject != null) {
						_store(relObject, stored);
					}
				}
			}
		}
		executeStatement(stmt, pClass.getProperties(), values);
		sessionCache.put(reference, object);
	}

	public Object load(Class<?> entity, Object id) {
		return _load(entity, id, sessionCache);
	}

	private Object _load(Class<?> entity, Object id,
			Map<Reference, Object> sessionCache) {
		try {
			Object instance = entity.newInstance();
			PersistentClass pClass = util.getMappingFor(entity);
			Reference reference = new Reference(pClass, id);
			Object cached = sessionCache.get(reference);
			if (cached != null) {
				return cached;
			}
			Log.d(getClass().getName(), "Loading " + reference);
			Object stmt = retrieveLoadStatement(pClass);
			if (id != null) {
				Type idType = util.getTypeFor(id.getClass());
				if (idType instanceof PersistentClass) {
					PersistentClass idPClass = (PersistentClass) idType;
					id = idPClass.getIdentifier().getValue(id);
				}
			}
			Map<Property, Object> values = executeQuery(stmt,
					pClass.getProperties(), pClass.getIdentifier(), id);
			sessionCache.put(reference, instance);

			for (Property property : pClass.getProperties()) {
				FetchMode fetchMode = property.getFetchMode();
				if (fetchMode != null) {
					switch (fetchMode) {
					case SEPARATE:
						values.put(
								property,
								_load(property.getJavaType(),
										values.get(property), sessionCache));
						break;
					default:
						throw new UnsupportedOperationException(
								"Fetchmode: " + fetchMode);
					}
				}
			}

			util.setValues(values, instance);
			return instance;
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		} catch (InstantiationException e) {
			throw new RuntimeException(e);
		}
	}

	private Object retrieveLoadStatement(PersistentClass pClass) {
		Object stmt = loadStatements.get(pClass);
		if (stmt == null) {
			String loadStatement = util.getLoadStatement(pClass,
					pClass.getProperties());
			Log.d(getClass().getName(), "Compiled new LOAD-Statement for "
					+ pClass.getMappedClass() + ": " + loadStatement);
			stmt = compileLoad(loadStatement);
			loadStatements.put(pClass, stmt);
		}
		return stmt;
	}

	protected abstract Map<Property, Object> executeQuery(Object stmt,
			List<Property> properties, Property identifier, Object id);

	protected abstract Object compileStore(String statement);

	protected abstract Object compileLoad(String statement);

	protected abstract Object compileCreateTable(String statement);

	protected abstract void executeStatement(Object stmt,
			Collection<Property> properties, Map<Property, Object> values);

	protected void executeStatement(Object stmt) {
		executeStatement(stmt, new ArrayList<Property>(0),
				new HashMap<Property, Object>(0));
	}
}
